Skip to content

Conversation

@six-standard
Copy link
Member

@six-standard six-standard commented Dec 17, 2025

🔥 변경 사항

  • 뱃지 생성기 및 뱃지 API를 추가하였습니다
  • 일부 코드에 존재하던 오류를 수정하고(체크박스 등), 재활용성을 높였습니다 (복사 버튼)

🏷 관련 이슈

X

📸 스크린샷 (UI 변경 시 필수)

스크린샷 2025-12-17 오후 8 25 22 스크린샷 2025-12-17 오후 8 25 57

📌 체크리스트

  • [O] 기능이 정상적으로 동작하는지 테스트 완료
    • 이 부분에 이슈가 하나 있습니다
      (사실 그냥 없애버릴 수도 있으나, 미리보기가 없으면 허전할 것 같아 일단 그대로 두었습니다)
  • [O] 코드 스타일 가이드 준수 여부 확인
  • [O] 관련 문서 업데이트 완료 (필요 시)

Summary by CodeRabbit

  • 새로운 기능

    • 뱃지 생성기 추가: 프로필 기반 맞춤형 뱃지 생성 기능 추가
    • 기본형/간단한 형식 레이아웃 선택 옵션 제공
    • 표시할 자산(조회수, 좋아요, 게시물) 선택 가능
    • 실시간 뱃지 미리보기 기능 제공
  • 스타일

    • 인터페이스 아이콘 크기 최적화

✏️ Tip: You can customize this high-level summary in your review settings.

@six-standard six-standard self-assigned this Dec 17, 2025
@six-standard six-standard added the enhancement New feature or request label Dec 17, 2025
@notion-workspace
Copy link

@coderabbitai
Copy link

coderabbitai bot commented Dec 17, 2025

Walkthrough

이 PR은 사용자의 프로필 뱃지를 동적으로 생성하는 OG 이미지 기반 기능을 도입합니다. API 라우트, 뱃지 구성 컴포넌트, 폰트/이미지 유틸리티, 그리고 클라이언트 UI 컴포넌트로 구성됩니다.

Changes

코호트 / 파일(들) 변경 요약
설정 및 의존성
next.config.mjs, package.json
localhost HTTP 원격 이미지 패턴 추가 및 @vercel/og 의존성 추가
뱃지 API 및 유틸리티
src/app/api/badge/route.tsx, src/app/api/badge/util.tsx
GET 라우트 핸들러로 뱃지 이미지 생성; fontStyle, loadFonts, createImageResponse 유틸 함수 추가
뱃지 UI 컴포넌트
src/app/api/badge/components/Posts.tsx, src/app/api/badge/components/PoweredBy.tsx, src/app/api/badge/components/Statistics.tsx, src/app/api/badge/components/Title.tsx, src/app/api/badge/components/index.ts
뱃지 레이아웃용 Posts, PoweredBy, Statistics, Title 컴포넌트 및 배럴 내보내기
뱃지 생성기 UI
src/app/components/BadgeGenerator/index.tsx
레이아웃 선택, 자산 필터링, 실시간 미리보기 및 복사 기능이 있는 클라이언트 컴포넌트
헤더 통합
src/app/components/Header/index.tsx
사용자 메뉴에 "뱃지 생성기" 버튼 추가
공유 컴포넌트 개선
src/shared/CopyButton.tsx, src/shared/Check.tsx, src/shared/Icon/index.tsx, src/shared/Icon/icons/index.ts, src/shared/index.ts
CopyButton 타입 확장(code 모드), Check onChange 핸들러 타입 수정, Icon tw 스타일 지원, Post/View 아이콘 추가, CopyButton 배럴 내보내기
기타 변경
src/app/components/QRCode/index.tsx, src/app/components/Section/index.tsx
CopyButton 임포트 위치 변경, Like 아이콘 크기 조정

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Client as BadgeGenerator<br/>(Client)
    participant API as /api/badge<br/>(Route Handler)
    participant FontService as Font Loader<br/>(loadFonts)
    participant OGService as ImageResponse<br/>(createImageResponse)
    participant Browser

    User->>Client: Configure (layout, assets)
    Client->>API: GET /api/badge?username=...&type=...&assets=...
    API->>API: Validate & parse params
    API->>FontService: loadFonts (Noto fonts)
    FontService-->>API: ArrayBuffer (font data)
    API->>OGService: createImageResponse(JSX node, options)
    OGService->>OGService: Compute dimensions<br/>(sizeTable)
    OGService->>OGService: Render React components<br/>(Title, Stats, Posts, etc.)
    OGService-->>API: ImageResponse (image bytes)
    API-->>Client: Image (PNG/SVG)
    Client->>Browser: Display preview
    User->>Browser: Copy HTML anchor
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • 폰트 로딩 및 Vercel OG 이미지 생성 로직 (src/app/api/badge/util.tsx): 복잡한 유틸리티 함수, 비동기 폰트 페칭, ImageResponse 구성
  • API 라우트 핸들러 (src/app/api/badge/route.tsx): 쿼리 파라미터 파싱, 조건부 컴포넌트 렌더링 로직
  • BadgeGenerator 상태 관리: React Query와 복수의 체크박스 토글 상태 조정
  • 통합 지점 (src/app/components/Header/index.tsx): 모달 및 컴포넌트 통합
  • 공유 컴포넌트 변경 (CopyButton, Check, Icon): 기존 인터페이스 확장

Possibly related PRs

Suggested reviewers

  • Nuung
  • Jihyun3478
  • ooheunda

Poem

🐰 뱃지 생성기 마법이 펼쳐졌네,
Vercel의 OG로 이미지가 춤을 춘다!
폰트 로딩, 컴포넌트 조합,
사용자의 프로필이 반짝반짝 ✨
헤더에 새로운 버튼이 살포시...

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 뱃지 기능 추가라는 주요 변경사항을 명확하게 요약하고 있으며, 변경 사항들(뱃지 생성기, API, 컴포넌트)이 모두 이 목표에 부합합니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/badge

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@six-standard
Copy link
Member Author

SVG 크기 수정, 옵션 수정 등 단순 수정이 많아서 파일이 좀 많아보이네요..
단순해보인다 싶은 것들은 건너띄고 새로 만들어진 파일들 위주로 리뷰해주시면 감사하겠습니다!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (4)
src/app/api/badge/components/Statistics.tsx (1)

20-39: 배열 인덱스를 key로 사용하는 것을 개선하세요.

Line 26에서 key={index}를 사용하고 있습니다. assets 배열의 순서가 변경될 가능성이 있다면 React의 재조정(reconciliation) 성능에 영향을 줄 수 있습니다.

item 자체를 key로 사용하는 것이 더 안전합니다:

-      {assets?.map((item, index) => (
-        <div tw="flex items-center" key={index}>
+      {assets?.map((item) => (
+        <div tw="flex items-center" key={item}>
src/app/api/badge/route.tsx (1)

48-91: 에러 처리 및 캐싱 헤더를 추가하세요.

createImageResponse 호출에 대한 에러 처리가 없으며, 이미지 응답에 캐싱 헤더가 설정되지 않아 성능에 영향을 줄 수 있습니다.

다음과 같이 에러 처리와 캐싱을 추가하세요:

 export async function GET(request: Request) {
   const { origin, searchParams } = new URL(request.url);
   // ... 파라미터 추출 코드
   
+  try {
     if (type === 'simple') {
-      return await createImageResponse(
+      const response = await createImageResponse(
         // ... JSX
       );
+      response.headers.set('Cache-Control', 'public, max-age=3600, s-maxage=3600');
+      return response;
     }

-    return await createImageResponse(
+    const response = await createImageResponse(
       // ... JSX
     );
+    response.headers.set('Cache-Control', 'public, max-age=3600, s-maxage=3600');
+    return response;
+  } catch (error) {
+    console.error('Badge generation failed:', error);
+    return NextResponse.json(
+      { error: 'Failed to generate badge' },
+      { status: 500 }
+    );
+  }
 }
src/app/components/BadgeGenerator/index.tsx (2)

97-102: 중복 로직 제거를 권장

97-102번 라인의 로직이 28-30번 라인에서 이미 계산된 selectedAssets와 중복됩니다. 이미 계산된 값을 재사용하는 것이 코드 유지보수성과 가독성 측면에서 더 좋습니다.

다음과 같이 수정하세요:

 <Image
   fill
   key={type}
-  src={`http://localhost:3000/api/badge?username=${profiles?.username}&size=2&type=${type}&assets=${Object.entries(
-    assets,
-  )
-    .filter((value) => value[1])
-    .map(([key]) => key)
-    .join(',')}`}
+  src={`${ENVS.BASE_URL}/api/badge?username=${profiles?.username}&size=2&type=${type}&assets=${selectedAssets.join(',')}`}
   alt="Preview"
 />

73-76: 긴 문자열 리터럴 가독성 개선 고려

HTML 코드 문자열이 한 줄에 너무 길어 가독성이 떨어집니다. 템플릿 리터럴을 여러 줄로 나누거나, 별도의 함수로 추출하는 것을 고려해보세요.

다음과 같이 리팩토링할 수 있습니다:

+const generateBadgeHTML = (username: string, type: string, assets: string[]) => {
+  const assetParam = assets.length ? `&assets=${assets.join(',')}` : '';
+  return `<a href="https://velog.io/@${username}">
+  <img src="${ENVS.BASE_URL}/api/badge?username=${username}&type=${type}${assetParam}" />
+</a>`;
+};
+
 export const BadgeGenerator = () => {
   // ... existing code ...
   
   return (
     <Layout title="뱃지 생성기">
       {/* ... */}
       <CopyButton
-        url={`<a href="https://velog.io/@${profiles?.username}">\n  <img src="http://localhost:3000/api/badge?username=${profiles?.username}&type=${type}${selectedAssets.length ? `&assets=${selectedAssets.join(',')}` : ''}" />\n</a>`}
+        url={generateBadgeHTML(profiles?.username || '', type, selectedAssets)}
         type="code"
         className="max-w-[650px]"
       />
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between de612ef and f7e8c42.

⛔ Files ignored due to path filters (7)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • public/NotoSansKR-Bold.ttf is excluded by !**/*.ttf
  • public/NotoSansKR-Medium.ttf is excluded by !**/*.ttf
  • public/velog.png is excluded by !**/*.png
  • src/shared/Icon/icons/Like.svg is excluded by !**/*.svg
  • src/shared/Icon/icons/Post.svg is excluded by !**/*.svg
  • src/shared/Icon/icons/View.svg is excluded by !**/*.svg
📒 Files selected for processing (18)
  • next.config.mjs (1 hunks)
  • package.json (1 hunks)
  • src/app/api/badge/components/Posts.tsx (1 hunks)
  • src/app/api/badge/components/PoweredBy.tsx (1 hunks)
  • src/app/api/badge/components/Statistics.tsx (1 hunks)
  • src/app/api/badge/components/Title.tsx (1 hunks)
  • src/app/api/badge/components/index.ts (1 hunks)
  • src/app/api/badge/route.tsx (1 hunks)
  • src/app/api/badge/util.tsx (1 hunks)
  • src/app/components/BadgeGenerator/index.tsx (1 hunks)
  • src/app/components/Header/index.tsx (2 hunks)
  • src/app/components/QRCode/index.tsx (1 hunks)
  • src/app/components/Section/index.tsx (1 hunks)
  • src/shared/Check.tsx (1 hunks)
  • src/shared/CopyButton.tsx (3 hunks)
  • src/shared/Icon/icons/index.ts (1 hunks)
  • src/shared/Icon/index.tsx (2 hunks)
  • src/shared/index.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-03-11T12:38:34.012Z
Learnt from: Nuung
Repo: Check-Data-Out/velog-dashboard-v2-fe PR: 23
File: src/components/auth-required/notice/index.tsx:18-27
Timestamp: 2025-03-11T12:38:34.012Z
Learning: React 컴포넌트에서 API 응답 데이터를 처리할 때는 데이터가 없거나 배열이 비어있는 경우(전체 기능 비활성화, DB 마이그레이션, DBMS 연결 문제 등)에 대비한 방어적 코딩이 필요하다. 특히 `data?.posts[0]`과 같이 배열의 요소에 접근하기 전에 `if (!data?.posts || data.posts.length === 0) return;`와 같은 가드 클로즈를 사용하여 런타임 오류를 방지해야 한다.

Applied to files:

  • src/app/api/badge/components/Posts.tsx
  • src/app/api/badge/route.tsx
📚 Learning: 2024-11-28T08:00:11.465Z
Learnt from: six-standard
Repo: Check-Data-Out/velog-dashboard-v2-fe PR: 2
File: src/components/Button.tsx:0-0
Timestamp: 2024-11-28T08:00:11.465Z
Learning: In `src/components/Button.tsx`, 컴포넌트에서만 사용되는 스타일 값은 Tailwind CSS 설정 파일이 아닌 컴포넌트 내부에서 하드코딩하는 것이 허용됩니다.

Applied to files:

  • src/shared/CopyButton.tsx
🧬 Code graph analysis (9)
src/app/components/Header/index.tsx (1)
src/app/components/BadgeGenerator/index.tsx (1)
  • BadgeGenerator (12-110)
src/app/components/BadgeGenerator/index.tsx (6)
src/constants/paths.constant.ts (1)
  • PATHS (1-15)
src/apis/user.request.ts (1)
  • me (15-15)
src/app/(auth-required)/layout.tsx (1)
  • Layout (7-24)
src/shared/Dropdown.tsx (1)
  • Dropdown (12-36)
src/shared/Check.tsx (1)
  • Check (10-29)
src/shared/CopyButton.tsx (1)
  • CopyButton (12-90)
src/app/api/badge/components/PoweredBy.tsx (2)
src/app/api/badge/util.tsx (1)
  • fontStyle (8-20)
src/constants/styles.constant.ts (1)
  • COLORS (38-45)
src/app/api/badge/components/Posts.tsx (3)
src/constants/styles.constant.ts (1)
  • COLORS (38-45)
src/app/api/badge/util.tsx (1)
  • fontStyle (8-20)
src/shared/Icon/index.tsx (1)
  • Icon (24-40)
src/app/api/badge/components/Statistics.tsx (4)
src/shared/Icon/index.tsx (2)
  • Icon (24-40)
  • NameType (4-4)
src/constants/styles.constant.ts (1)
  • COLORS (38-45)
src/app/api/badge/util.tsx (1)
  • fontStyle (8-20)
src/utils/number.util.ts (1)
  • parseNumber (8-13)
src/app/api/badge/util.tsx (1)
src/constants/styles.constant.ts (2)
  • FONTS (13-35)
  • COLORS (38-45)
src/shared/CopyButton.tsx (1)
src/app/components/Rank.tsx (1)
  • IProp (6-12)
src/app/api/badge/components/Title.tsx (2)
src/app/api/badge/util.tsx (1)
  • fontStyle (8-20)
src/constants/styles.constant.ts (1)
  • COLORS (38-45)
src/app/api/badge/route.tsx (5)
src/app/api/badge/util.tsx (1)
  • createImageResponse (35-70)
src/app/api/badge/components/Title.tsx (1)
  • Title (12-23)
src/app/api/badge/components/Statistics.tsx (1)
  • Statistics (20-40)
src/app/api/badge/components/PoweredBy.tsx (1)
  • PoweredBy (6-25)
src/app/api/badge/components/Posts.tsx (1)
  • Posts (18-42)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: process-e2e-test
🔇 Additional comments (15)
src/shared/Check.tsx (1)

6-6: 타입 수정이 올바릅니다.

onChange 핸들러가 실제로 <input> 엘리먼트(26번 라인)에 연결되므로, HTMLDivElement에서 HTMLInputElement로 타입을 변경한 것이 정확합니다. 이 변경으로 타입 안정성이 개선되었습니다.

src/app/components/Section/index.tsx (1)

55-55: LGTM!

아이콘 크기 조정이 적절하며, 다양한 브레이크포인트에서 일관성 있게 적용되었습니다.

src/app/api/badge/components/PoweredBy.tsx (1)

1-25: LGTM!

컴포넌트 구조가 명확하고 @vercel/og의 JSX 스타일을 올바르게 사용하고 있습니다. react/no-unknown-property 비활성화는 tw prop 사용을 위해 필요합니다.

src/app/components/QRCode/index.tsx (1)

10-10: LGTM!

CopyButton을 공유 컴포넌트로 리팩토링하여 재사용성이 향상되었습니다. 배럴 export를 통한 import 경로 개선이 적절합니다.

src/shared/index.ts (1)

9-9: LGTM!

CopyButton을 배럴 export에 추가하여 공유 컴포넌트로 접근성을 개선했습니다.

src/shared/Icon/icons/index.ts (1)

10-11: LGTM!

새로운 아이콘 export가 기존 패턴과 일관되게 추가되었습니다.

src/app/components/Header/index.tsx (1)

128-136: LGTM! 뱃지 생성기 버튼 통합이 잘 되었습니다.

QR 로그인 버튼과 동일한 패턴을 따라 구현되어 일관성이 유지됩니다. 모달 오픈 전 메뉴를 닫는 로직도 적절합니다.

src/app/api/badge/components/index.ts (1)

1-4: LGTM! 표준 barrel export 패턴입니다.

배지 컴포넌트들을 깔끔하게 재내보내고 있습니다.

src/shared/Icon/index.tsx (1)

6-10: LGTM! tw prop 지원이 잘 추가되었습니다.

OG 이미지 생성을 위한 tw prop 지원이 타입 안전하게 구현되었습니다. 기존 API와의 호환성도 유지됩니다.

src/shared/CopyButton.tsx (2)

37-64: LGTM! 코드 타입 복사 버튼이 잘 구현되었습니다.

새로운 type='code' 옵션이 기존 기능을 유지하면서 깔끔하게 추가되었습니다. twMerge를 사용한 className 병합도 적절합니다.


66-89: 기본 타입 버튼의 리팩토링이 잘 되었습니다.

twMerge를 통한 className 관리와 rest props 스프레딩으로 재사용성이 향상되었습니다.

src/app/api/badge/components/Title.tsx (1)

12-22: 이 리뷰 코멘트는 부정확합니다.

Title 컴포넌트의 모든 사용처(route.tsx 62번, 78번 줄)에서 username={DATA.username} (하드코딩된 'six-standard')과 origin={origin} (URL 객체에서 제공)을 전달하고 있습니다. 따라서 Title 컴포넌트가 받는 props는 항상 유효한 값이며, 빈 문자열이 될 수 없습니다. 컴포넌트 레벨에서의 추가 검증이 필요하지 않습니다.

Likely an incorrect or invalid review comment.

src/app/components/BadgeGenerator/index.tsx (3)

12-19: 상태 관리 및 이벤트 핸들러 구현 양호

상태 관리와 체크박스 이벤트 핸들러가 올바르게 구현되었습니다. currentTarget을 사용하여 이벤트 버블링 문제를 방지하고, 함수형 setState로 상태를 안전하게 업데이트하고 있습니다.


21-26: useQuery 설정 적절함

사용자 프로필 데이터를 가져오는 쿼리 설정이 적절합니다. 5분의 staleTime은 자주 변경되지 않는 프로필 데이터에 적합하며, retry 설정도 합리적입니다.


38-45: as string 타입 단언은 이 사용 사례에서 안전합니다

Dropdown 컴포넌트 구현을 확인한 결과, 현재 코드의 타입 단언은 안전합니다. 제공된 옵션이 ['기본 보기', 'default'] 형식(3번째 매개변수 없음)이므로, Dropdown은 항상 두 번째 요소(value[1])인 문자열을 반환합니다. 따라서 as string 타입 단언은 정당하며 런타임 오류가 발생할 가능성이 없습니다.

remotePatterns: [
{ protocol: 'https', hostname: 'velog.velcdn.com', pathname: '**' },
{ protocol: 'https', hostname: 'images.velog.io', pathname: '**' },
{ protocol: 'http', hostname: 'localhost', pathname: '**' },
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

프로덕션 환경에서 localhost 패턴이 활성화되지 않도록 확인하세요.

HTTP localhost 패턴은 개발 환경에서만 필요합니다. 프로덕션 배포 시 보안 취약점이 될 수 있으므로, 환경 변수를 사용하여 개발 환경에서만 활성화되도록 제한하는 것을 권장합니다.

다음과 같이 수정하는 것을 고려하세요:

  images: {
    remotePatterns: [
      { protocol: 'https', hostname: 'velog.velcdn.com', pathname: '**' },
      { protocol: 'https', hostname: 'images.velog.io', pathname: '**' },
-      { protocol: 'http', hostname: 'localhost', pathname: '**' },
+      ...(process.env.NODE_ENV === 'development' 
+        ? [{ protocol: 'http', hostname: 'localhost', pathname: '**' }]
+        : []),
    ],
  },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{ protocol: 'http', hostname: 'localhost', pathname: '**' },
...(process.env.NODE_ENV === 'development'
? [{ protocol: 'http', hostname: 'localhost', pathname: '**' }]
: []),
🤖 Prompt for AI Agents
In next.config.mjs around line 26, the images remotePatterns entry allows
protocol:'http' hostname:'localhost' which should only be enabled in
development; modify the config to add the localhost/http pattern conditionally
based on an environment variable (e.g. process.env.NODE_ENV === 'development' or
a specific DEV flag) so that the localhost pattern is included only when that
flag is true, keeping production config free of the localhost entry and ensuring
remotePatterns is built from a base array plus the conditional localhost
pattern.

"@sentry/nextjs": "^8.47.0",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/react-query": "^5.69.0",
"@vercel/og": "^0.8.6",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

@vercel/og 패키지 버전 확인 필요

@vercel/og의 최신 버전은 0.8.5입니다. package.json의 ^0.8.6 버전 지정이 실제 존재하는 버전과 맞지 않습니다. ^0.8.6는 0.8.6 이상 0.9.0 미만의 버전을 요구하는데, 현재 0.8.6은 NPM 레지스트리에 공개되지 않았습니다. 버전을 ^0.8.5로 수정하거나 최신 버전이 릴리스되었는지 확인하세요.

🤖 Prompt for AI Agents
package.json around line 31: the dependency "@vercel/og": "^0.8.6" references a
non-existent 0.8.6 release; change it to a valid published version (e.g.,
"@vercel/og": "^0.8.5") or query the npm registry for the actual latest tag and
update the version range accordingly, then run your package manager
(npm/yarn/pnpm) to install and verify the lockfile updates.

Comment on lines +18 to +42
export const Posts = ({ posts }: IProps) => {
return (
<div style={{ gap: 14 }} tw="flex flex-col w-full h-[218px]">
{posts.map((item) => (
<div
key={item.id}
tw={`flex items-center w-full h-[43px] rounded-[4px] p-[12px] bg-[${COLORS.BG.SUB}]`}
>
<span {...fontStyle('TITLE', '5', COLORS.TEXT.MAIN)}>{item.title}</span>
<span {...fontStyle('SUBTITLE', '5', COLORS.TEXT.ALT, 'ml-[10px]')}>
{item.createdAt}
</span>
<div tw="ml-auto w-fit flex items-center">
<span {...fontStyle('SUBTITLE', '5', COLORS.TEXT.ALT, `mr-[4px]`)}>
{item.viewCount} / {item.viewDiff}{' '}
<span tw={`text-[${COLORS.PRIMARY.SUB}] ml-[2px] mr-[4px]`}></span> /{' '}
</span>
<Icon name="Like" size={14} color={COLORS.TEXT.ALT} tw="mr-[6px]" />
<span {...fontStyle('SUBTITLE', '5', COLORS.TEXT.ALT)}>{item.likeCount}</span>
</div>
</div>
))}
</div>
);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

posts 배열에 대한 방어적 코딩을 추가하세요.

Line 21에서 posts.map을 호출하기 전에 posts가 비어있거나 undefined인 경우를 처리하지 않습니다. DB 마이그레이션, 연결 문제 등으로 데이터가 없을 경우 런타임 오류가 발생할 수 있습니다.

Based on learnings, 배열 접근 전 가드 클로즈가 필요합니다.

다음과 같이 가드 클로즈를 추가하세요:

 export const Posts = ({ posts }: IProps) => {
+  if (!posts || posts.length === 0) {
+    return (
+      <div style={{ gap: 14 }} tw="flex flex-col w-full h-[218px] items-center justify-center">
+        <span {...fontStyle('SUBTITLE', '5', COLORS.TEXT.ALT)}>
+          게시물이 없습니다
+        </span>
+      </div>
+    );
+  }
+
   return (
     <div style={{ gap: 14 }} tw="flex flex-col w-full h-[218px]">
🤖 Prompt for AI Agents
In src/app/api/badge/components/Posts.tsx around lines 18 to 42, the component
calls posts.map without guarding against posts being undefined or null; add a
defensive check (e.g., default posts to an empty array via posts = posts ?? []
or use (posts || []).map(...) or return a safe empty state early) so the
component never calls map on undefined, keep the rendered structure the same
when posts is empty, and ensure the prop typing still accepts undefined if
necessary.

Comment on lines +7 to +46
const DATA = {
username: 'six-standard',
totalViews: 12345,
totalLikes: 6789,
totalPosts: 123,
posts: [
{
id: '1',
title: '제목',
createdAt: '2025-12-16',
viewCount: 123,
viewDiff: 456,
likeCount: 789,
},
{
id: '1',
title: '제목',
createdAt: '2025-12-16',
viewCount: 123,
viewDiff: 456,
likeCount: 789,
},
{
id: '1',
title: '제목',
createdAt: '2025-12-16',
viewCount: 123,
viewDiff: 456,
likeCount: 789,
},
{
id: '1',
title: '제목',
createdAt: '2025-12-16',
viewCount: 123,
viewDiff: 456,
likeCount: 789,
},
],
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

정적 DATA를 실제 API 호출로 교체해야 합니다.

Lines 7-46의 DATA 객체가 하드코딩되어 있으며, Line 51에서 추출한 username 파라미터가 실제로 사용되지 않습니다. 모든 사용자에게 동일한 데이터를 반환하게 됩니다.

또한 Lines 13, 22, 30, 38의 모든 post가 동일한 id: '1'을 가지고 있어 React에서 key 중복 경고가 발생합니다.

username 파라미터를 기반으로 실제 사용자 데이터를 조회하도록 수정이 필요합니다:

-const DATA = {
-  username: 'six-standard',
-  totalViews: 12345,
-  ...
-};
-
 export async function GET(request: Request) {
   const { origin, searchParams } = new URL(request.url);
   const size = Number(searchParams.get('size')) || 2;
   const username = searchParams.get('username') || '';
   const type = (searchParams.get('type') as 'default' | 'simple') || 'default';
   const assets = searchParams.get('assets')?.split(',') as ('views' | 'likes' | 'posts')[];

   if (!username) {
     return NextResponse.json({ error: "'username' parameter is required" }, { status: 400 });
   }
+
+  // 실제 사용자 데이터 조회
+  const userData = await fetchUserData(username);
+  if (!userData) {
+    return NextResponse.json({ error: 'User not found' }, { status: 404 });
+  }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +8 to +20
export const fontStyle = <T extends keyof typeof FONTS>(
type: T,
index: keyof (typeof FONTS)[T],
color?: string,
tw?: string,
) => {
const [size, { lineHeight, fontWeight }] = FONTS[type][index] as fontType;

return {
style: { fontFamily: `Noto${fontWeight}` },
tw: `text-[${parseInt(size)}px] leading-[${parseInt(lineHeight)}px] text-[${color}] ${tw}`,
};
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

@vercel/og Satori dynamic Tailwind classes support

💡 Result:

Short answer: @vercel/og (Satori) supports Tailwind utilities via the tw prop, but "dynamic" Tailwind class names (constructed at runtime/with interpolation or arbitrary values) are a Tailwind build-time limitation — they won't be emitted unless you ensure those class names exist at build time (e.g., safelist/patterns, explicit full class strings) or you use inline styles / CSS variables instead.

Key sources:

  • @vercel/og / Tailwind support (tw prop) and limitations reported by users. [1][2]
  • Satori is the renderer used under the hood (Satori supports inline CSS; Tailwind is an integration on top). [3]
  • Tailwind docs: class detection, dynamic-class limitation, and safelist/patterns as the recommended workaround. [4]

If you want, I can show examples:

  • using tw with explicit classes (works),
  • safelisting/patterns in tailwind.config.js (for many possible values),
  • or switching to inline style/CSS variables for truly dynamic colors/values.

fontStyletw prop에서 동적 Tailwind 클래스 사용 불가능

text-[${parseInt(size)}px] 같은 템플릿 리터럴로 생성된 동적 클래스명은 Tailwind의 빌드타임 제한으로 인해 최종 CSS에 포함되지 않습니다. @vercel/og의 Satori 엔진은 Tailwind을 지원하지만, 런타임에 생성된 클래스명은 인식되지 않아 스타일이 적용되지 않습니다.

해결책:

  • tailwind.config.jssafelist에 가능한 모든 클래스를 등록하거나
  • 인라인 스타일(style 객체)로 동적 값을 직접 적용하거나
  • CSS 변수를 사용하는 방식으로 변경
🤖 Prompt for AI Agents
In src/app/api/badge/util.tsx around lines 8 to 20, the template-literal
Tailwind classes like text-[${parseInt(size)}px] and
leading-[${parseInt(lineHeight)}px] are generated at runtime and won't be picked
up by Tailwind/Satori; change the function to apply dynamic fontSize, lineHeight
and color via the style object (e.g., set fontSize, lineHeight, color alongside
fontFamily) and keep only static Tailwind classes in the tw string, or
alternatively add all possible generated classes to tailwind.config.js safelist
if you must keep them; update the returned object accordingly so dynamic values
are applied via inline styles or CSS variables rather than runtime Tailwind
class names.

};
};

export const loadFonts = async (url: string) => await (await fetch(new URL(url)))?.arrayBuffer();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

폰트 로딩 오류 처리가 누락되었습니다.

loadFonts 함수가 fetch 실패 시 undefined를 반환하지만, 호출하는 쪽에서 이를 처리하지 않습니다. 네트워크 오류나 파일 누락 시 런타임 에러가 발생할 수 있습니다.

다음과 같이 오류 처리를 추가하세요:

-export const loadFonts = async (url: string) => await (await fetch(new URL(url)))?.arrayBuffer();
+export const loadFonts = async (url: string): Promise<ArrayBuffer> => {
+  try {
+    const response = await fetch(new URL(url));
+    if (!response.ok) {
+      throw new Error(`Failed to load font from ${url}: ${response.statusText}`);
+    }
+    return await response.arrayBuffer();
+  } catch (error) {
+    throw new Error(`Font loading failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
+  }
+};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const loadFonts = async (url: string) => await (await fetch(new URL(url)))?.arrayBuffer();
export const loadFonts = async (url: string): Promise<ArrayBuffer> => {
try {
const response = await fetch(new URL(url));
if (!response.ok) {
throw new Error(`Failed to load font from ${url}: ${response.statusText}`);
}
return await response.arrayBuffer();
} catch (error) {
throw new Error(`Font loading failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
🤖 Prompt for AI Agents
In src/app/api/badge/util.tsx around line 22, loadFonts currently does a blind
fetch and may return undefined on network or HTTP errors; add proper error
handling by awaiting fetch once, checking response.ok, and throwing a
descriptive Error (including the URL and HTTP status) when fetch fails or
response is not ok, otherwise return the awaited arrayBuffer; ensure any thrown
errors propagate so callers can catch them or adjust the API to return a
Result/nullable explicitly.

Comment on lines +35 to +70
export const createImageResponse = async (node: React.ReactNode, options: options) => {
const { origin, type, size } = options;

const NotoBold = await loadFonts(origin + '/NotoSansKR-Bold.ttf');
const NotoMedium = await loadFonts(origin + '/NotoSansKR-Medium.ttf');
const [width, height, padding] = sizeTable[type];

return new ImageResponse(
(
<div
style={{ width: width * size, height: height * size }}
tw="flex items-center justify-center"
>
<div
style={{
width,
height,
padding: `${padding - 5}px ${padding}px`,
transform: `scale(${size})`,
}}
tw={`flex bg-[${COLORS.BG.MAIN}]`}
>
{node}
</div>
</div>
),
{
width: width * size,
height: height * size,
fonts: [
{ data: NotoMedium, name: 'Noto500', weight: 500 },
{ data: NotoBold, name: 'Noto700', weight: 700 },
],
},
);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

폰트 로딩 실패 시 처리를 추가하세요.

createImageResponse에서 폰트 로딩이 실패하면 ImageResponseundefined가 전달되어 런타임 에러가 발생합니다. Line 38-39의 폰트 로딩 결과를 검증해야 합니다.

다음과 같이 검증 로직을 추가하세요:

export const createImageResponse = async (node: React.ReactNode, options: options) => {
  const { origin, type, size } = options;

  const NotoBold = await loadFonts(origin + '/NotoSansKR-Bold.ttf');
  const NotoMedium = await loadFonts(origin + '/NotoSansKR-Medium.ttf');
+
+  if (!NotoBold || !NotoMedium) {
+    throw new Error('Failed to load required fonts');
+  }
+
  const [width, height, padding] = sizeTable[type];

  return new ImageResponse(
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const createImageResponse = async (node: React.ReactNode, options: options) => {
const { origin, type, size } = options;
const NotoBold = await loadFonts(origin + '/NotoSansKR-Bold.ttf');
const NotoMedium = await loadFonts(origin + '/NotoSansKR-Medium.ttf');
const [width, height, padding] = sizeTable[type];
return new ImageResponse(
(
<div
style={{ width: width * size, height: height * size }}
tw="flex items-center justify-center"
>
<div
style={{
width,
height,
padding: `${padding - 5}px ${padding}px`,
transform: `scale(${size})`,
}}
tw={`flex bg-[${COLORS.BG.MAIN}]`}
>
{node}
</div>
</div>
),
{
width: width * size,
height: height * size,
fonts: [
{ data: NotoMedium, name: 'Noto500', weight: 500 },
{ data: NotoBold, name: 'Noto700', weight: 700 },
],
},
);
};
export const createImageResponse = async (node: React.ReactNode, options: options) => {
const { origin, type, size } = options;
const NotoBold = await loadFonts(origin + '/NotoSansKR-Bold.ttf');
const NotoMedium = await loadFonts(origin + '/NotoSansKR-Medium.ttf');
if (!NotoBold || !NotoMedium) {
throw new Error('Failed to load required fonts');
}
const [width, height, padding] = sizeTable[type];
return new ImageResponse(
(
<div
style={{ width: width * size, height: height * size }}
tw="flex items-center justify-center"
>
<div
style={{
width,
height,
padding: `${padding - 5}px ${padding}px`,
transform: `scale(${size})`,
}}
tw={`flex bg-[${COLORS.BG.MAIN}]`}
>
{node}
</div>
</div>
),
{
width: width * size,
height: height * size,
fonts: [
{ data: NotoMedium, name: 'Noto500', weight: 500 },
{ data: NotoBold, name: 'Noto700', weight: 700 },
],
},
);
};
🤖 Prompt for AI Agents
In src/app/api/badge/util.tsx around lines 35 to 70, the code loads NotoBold and
NotoMedium but does not check whether the loads succeeded, which can pass
undefined into ImageResponse and cause runtime errors; update the function to
validate the loaded font buffers immediately after the await calls and handle
failures by either (1) throwing a clear, descriptive error (or logging and
returning an error response) if a required font is missing, or (2) supplying a
safe fallback (e.g., built-in system font buffer or omitting the fonts entry) so
that ImageResponse never receives undefined; finally ensure the fonts array only
includes entries with defined data and keep the existing names/weights.

import { useQuery } from '@tanstack/react-query';
import Image from 'next/image';
import { FormEvent, useState } from 'react';
// import { ENVS } from '@/constants';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find ENVS constant definition
echo "=== Searching for ENVS constant definition ==="
rg -t ts -t tsx "export const ENVS|export.*ENVS" --max-count=10

# Check BadgeGenerator file
echo -e "\n=== Checking BadgeGenerator file ==="
fd "BadgeGenerator.*index" --extension tsx

Repository: Check-Data-Out/velog-dashboard-v2-fe

Length of output: 209


🏁 Script executed:

#!/bin/bash
# Find BadgeGenerator file
echo "=== Finding BadgeGenerator file ==="
find . -type f -name "*BadgeGenerator*" 

# Search for constants directory
echo -e "\n=== Finding constants directory ==="
find . -type d -name "constants"

# Search for ENVS in all files
echo -e "\n=== Searching for ENVS string in codebase ==="
grep -r "ENVS" --include="*.ts" --include="*.tsx" 2>/dev/null | head -20

# Search for localhost:3000
echo -e "\n=== Searching for localhost:3000 ==="
grep -r "localhost:3000" --include="*.ts" --include="*.tsx" 2>/dev/null

Repository: Check-Data-Out/velog-dashboard-v2-fe

Length of output: 2129


🏁 Script executed:

#!/bin/bash
# Read ENVS constant definition
echo "=== ENVS constant definition ==="
cat src/constants/env.constant.ts

echo -e "\n=== BadgeGenerator component ==="
cat src/app/components/BadgeGenerator/index.tsx

Repository: Check-Data-Out/velog-dashboard-v2-fe

Length of output: 4649


ENVS 상수 import 주석 해제 및 하드코딩된 URL 교체

import { ENVS } from '@/constants'; 주석을 해제하고, 두 곳의 http://localhost:3000 하드코딩된 URL을 ENVS.BASE_URL으로 변경하세요. QRCode 컴포넌트에서 const url = \${ENVS.BASE_URL}/api/qr-login?token=${data?.token}`;`처럼 올바르게 구현한 패턴을 따르세요.

🤖 Prompt for AI Agents
In src/app/components/BadgeGenerator/index.tsx around line 6, uncomment the
import "import { ENVS } from '@/constants';" and replace both occurrences of the
hardcoded "http://localhost:3000" with ENVS.BASE_URL; for QRCode generation
follow the existing pattern so the URL becomes something like
`${ENVS.BASE_URL}/api/qr-login?token=${data?.token}` and update any other link
construction similarly to use ENVS.BASE_URL instead of the literal localhost
string.

<div className="flex gap-2 flex-col">
<span className="text-TEXT-MAIN text-TITLE-5">HTML 코드 </span>
<CopyButton
url={`<a href="https://velog.io/@${profiles?.username}">\n <img src="http://localhost:3000/api/badge?username=${profiles?.username}&type=${type}${selectedAssets.length ? `&assets=${selectedAssets.join(',')}` : ''}" />\n</a>`}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

프로덕션 환경에서 동작하지 않는 하드코딩된 URL

73번과 97번 라인에서 http://localhost:3000이 하드코딩되어 있습니다. 이는 프로덕션 환경에서 올바르게 작동하지 않으며, 배포 시 심각한 문제를 일으킵니다.

환경 변수를 사용하도록 수정하세요:

+'use client';
+
 import { useQuery } from '@tanstack/react-query';
 import Image from 'next/image';
 import { FormEvent, useState } from 'react';
-// import { ENVS } from '@/constants';
+import { ENVS } from '@/constants';
 import { twMerge } from 'tailwind-merge';

그리고 하드코딩된 URL을 환경 변수로 대체하세요:

 <CopyButton
-  url={`<a href="https://velog.io/@${profiles?.username}">\n  <img src="http://localhost:3000/api/badge?username=${profiles?.username}&type=${type}${selectedAssets.length ? `&assets=${selectedAssets.join(',')}` : ''}" />\n</a>`}
+  url={`<a href="https://velog.io/@${profiles?.username}">\n  <img src="${ENVS.BASE_URL}/api/badge?username=${profiles?.username}&type=${type}${selectedAssets.length ? `&assets=${selectedAssets.join(',')}` : ''}" />\n</a>`}
   type="code"
   className="max-w-[650px]"
 />
 <Image
   fill
   key={type}
-  src={`http://localhost:3000/api/badge?username=${profiles?.username}&size=2&type=${type}&assets=${Object.entries(
-    assets,
-  )
-    .filter((value) => value[1])
-    .map(([key]) => key)
-    .join(',')}`}
+  src={`${ENVS.BASE_URL}/api/badge?username=${profiles?.username}&size=2&type=${type}&assets=${selectedAssets.join(',')}`}
   alt="Preview"
 />

Also applies to: 97-97

🤖 Prompt for AI Agents
In src/app/components/BadgeGenerator/index.tsx around lines 73 and 97, the API
base URL is hardcoded to http://localhost:3000 which will break in production;
replace both occurrences with a configurable environment variable (e.g. use
process.env.NEXT_PUBLIC_API_BASE or a similarly named NEXT_PUBLIC_* var exposed
to the client), defaulting to an empty string or safer fallback if undefined,
and construct the badge URL using that env var (ensuring to update both lines 73
and 97); make sure the env var is documented and available in production builds.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants